Skip to content

feat: add Windows title bar theme support#136

Merged
erictli merged 3 commits into
erictli:mainfrom
chengcheng84:fix/titlebar
May 5, 2026
Merged

feat: add Windows title bar theme support#136
erictli merged 3 commits into
erictli:mainfrom
chengcheng84:fix/titlebar

Conversation

@chengcheng84
Copy link
Copy Markdown
Contributor

@chengcheng84 chengcheng84 commented Apr 6, 2026

Before

2026-04-06.093820.mp4

Now

2026-04-06.093437.mp4

Summary by CodeRabbit

  • New Features
    • Title bar now syncs with light/dark theme in real time for a more cohesive look.
    • Windows: system title bar and accent colors now follow the app's resolved background color, including custom colors, and update whenever theme or custom colors change.
    • Non-Windows: only the in-app UI updates; no system title-bar changes are applied.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 6, 2026

📝 Walkthrough

Walkthrough

Adds a Windows-only title-bar theming module and Tauri command set_title_bar_theme(app, is_dark, r, g, b) that applies immersive dark mode and caption/border colors (from provided RGB) to windows labeled "main" or starting with "preview-"; invoked from the React ThemeContext; non-Windows returns Ok(()).

Changes

Title Bar Theming (frontend + backend)

Layer / File(s) Summary
API Signature
src-tauri/src/lib.rs
Tauri command signature extended to accept r: u8, g: u8, b: u8 in set_title_bar_theme(app, is_dark, r, g, b).
Windows Implementation
src-tauri/src/lib.rs (Windows-only module)
New windows_title_bar module computes COLORREF from (r,g,b) and calls DwmSetWindowAttribute to set immersive dark mode and caption/border colors.
Command Wiring
src-tauri/src/lib.rs
Registered set_title_bar_theme in the invoke_handler; on Windows iterates app.webview_windows() and applies theme to windows labeled "main" or starting with "preview-"; on non-Windows is a no-op returning Ok(()).
Frontend Invoke
src/context/ThemeContext.tsx
Added parseCssColorToRgb helper and calls invoke("set_title_bar_theme", { isDark, r, g, b }) (swallows errors) whenever resolvedTheme or custom color maps change, using bg-secondary as source color.
Tests / Docs
(none)
No test or documentation changes in this diff.

Sequence Diagram

sequenceDiagram
    participant React as React Theme Context
    participant Tauri as Tauri Command Handler
    participant WinAPI as Windows API (DwmSetWindowAttribute)
    participant Window as Native Window Title Bar

    React->>Tauri: invoke("set_title_bar_theme", {isDark, r, g, b})
    activate Tauri
    Tauri->>Tauri: enumerate webview_windows() (labels "main"/"preview-*")
    Tauri->>WinAPI: apply_title_bar_theme(window, isDark, (r,g,b))
    activate WinAPI
    WinAPI->>Window: set immersive dark + caption/border colors
    deactivate WinAPI
    Tauri-->>React: Ok(())
    deactivate Tauri
Loading

Estimated Code Review Effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I hopped from React to native calls,
Dark bars and captions answering my calls.
RGB carrots painted with care,
Windows gleam, title bars fair—
A tiny rabbit's theming prayer 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add Windows title bar theme support' accurately and concisely describes the main change: adding Windows-specific title bar theme functionality with color customization.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src-tauri/src/lib.rs (2)

3892-3903: Preview windows won't receive title bar theme updates.

The command only applies the theme to the "main" window. Preview windows (labeled "preview-*") created via create_preview_window won't have their title bars themed, causing visual inconsistency when editing files outside the notes folder.

Consider iterating over all windows or accepting an optional window label:

🔧 Suggested fix to theme all windows
 #[tauri::command]
 fn set_title_bar_theme(app: AppHandle, is_dark: bool) -> Result<(), String> {
     #[cfg(target_os = "windows")]
     {
-        if let Some(window) = app.get_webview_window("main") {
-            windows_title_bar::apply_title_bar_theme(&window, is_dark);
+        for (_, window) in app.webview_windows() {
+            windows_title_bar::apply_title_bar_theme(&window, is_dark);
         }
     }
     let _ = app;
     let _ = is_dark;
     Ok(())
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/lib.rs` around lines 3892 - 3903, The set_title_bar_theme
command currently only themes the "main" window; update it to apply the theme to
all relevant windows (e.g., the "main" window and any preview windows created by
create_preview_window with labels starting with "preview-") by iterating over
app.windows() (or app.hooks/windows API) and calling
windows_title_bar::apply_title_bar_theme(&window, is_dark) for each matching
window label, or alternatively extend set_title_bar_theme to accept an optional
window_label parameter and apply the theme only to that label if provided;
adjust references to set_title_bar_theme and create_preview_window accordingly
so preview windows receive updates too.

3705-3708: Initial title bar theme doesn't respect system preference.

At startup, apply_title_bar_theme is called with is_dark: false, regardless of the actual system theme. This may cause a brief flash of light title bar before the frontend's ThemeContext invokes set_title_bar_theme with the correct resolved theme.

Consider reading the system theme preference here, or deferring this call until after the frontend initializes:

🔧 Suggested fix to respect system preference at startup
 #[cfg(target_os = "windows")]
 {
-    windows_title_bar::apply_title_bar_theme(&main_window, false);
+    // Let the frontend apply the theme after resolving user/system preference
+    // to avoid a brief flash of incorrect title bar color
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src-tauri/src/lib.rs` around lines 3705 - 3708, The Windows title bar is
forced to light by calling
windows_title_bar::apply_title_bar_theme(&main_window, false) at startup; change
this to respect system preference by querying the system theme and passing the
actual dark/light boolean (or defer the call and let the frontend call
set_title_bar_theme after ThemeContext resolves). Update the #[cfg(target_os =
"windows")] block: replace the hardcoded false with the system theme check (or
remove the call and rely on set_title_bar_theme) so apply_title_bar_theme and
main_window use the correct initial is_dark value.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src-tauri/src/lib.rs`:
- Around line 3892-3903: The set_title_bar_theme command currently only themes
the "main" window; update it to apply the theme to all relevant windows (e.g.,
the "main" window and any preview windows created by create_preview_window with
labels starting with "preview-") by iterating over app.windows() (or
app.hooks/windows API) and calling
windows_title_bar::apply_title_bar_theme(&window, is_dark) for each matching
window label, or alternatively extend set_title_bar_theme to accept an optional
window_label parameter and apply the theme only to that label if provided;
adjust references to set_title_bar_theme and create_preview_window accordingly
so preview windows receive updates too.
- Around line 3705-3708: The Windows title bar is forced to light by calling
windows_title_bar::apply_title_bar_theme(&main_window, false) at startup; change
this to respect system preference by querying the system theme and passing the
actual dark/light boolean (or defer the call and let the frontend call
set_title_bar_theme after ThemeContext resolves). Update the #[cfg(target_os =
"windows")] block: replace the hardcoded false with the system theme check (or
remove the call and rely on set_title_bar_theme) so apply_title_bar_theme and
main_window use the correct initial is_dark value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d1fc1545-3c61-42a6-94ea-fc65d97dbff4

📥 Commits

Reviewing files that changed from the base of the PR and between 6d443e9 and 9ea3893.

📒 Files selected for processing (2)
  • src-tauri/src/lib.rs
  • src/context/ThemeContext.tsx

chengcheng84 and others added 2 commits April 6, 2026 09:48
…order

The hardcoded caption color literals were encoded as 0x00RRGGBB but
DwmSetWindowAttribute expects COLORREF (0x00BBGGRR), so the title bar
was a few bits off from --color-bg-secondary. With erictli#134 letting users
customize theme colors, the gap also widened beyond the defaults.

Pass the resolved bg-secondary color from the frontend as an RGB triple
and build the COLORREF correctly in Rust.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/context/ThemeContext.tsx (2)

482-489: 💤 Low value

Consider logging the error instead of silently swallowing it.

Other backend invocations in this file (saveThemeSettings, saveFontSettings, setTextDirection, setEditorWidth, setCustomColor, etc.) all log failures via console.error. Swallowing errors here makes future debugging of title-bar regressions on Windows harder, with no real benefit since this is dev/console-only output.

♻️ Suggested change
     if (rgb) {
       invoke("set_title_bar_theme", {
         isDark: resolvedTheme === "dark",
         r: rgb[0],
         g: rgb[1],
         b: rgb[2],
-      }).catch(() => {});
+      }).catch((err) =>
+        console.error("Failed to sync title bar theme:", err),
+      );
     }

As per coding guidelines: Implement error handling with user-friendly messages.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/context/ThemeContext.tsx` around lines 482 - 489, The catch on the
invoke("set_title_bar_theme", ...) call is swallowing errors; update the catch
to log the error (e.g., console.error) including context such as the function
call and values like resolvedTheme and rgb so failures in set_title_bar_theme
are visible during debugging; locate the block in ThemeContext.tsx where
invoke("set_title_bar_theme", { isDark: resolvedTheme === "dark", r: rgb[0], g:
rgb[1], b: rgb[2] }) is called and replace the empty .catch(() => {}) with a
.catch(err => console.error("set_title_bar_theme failed", { err, resolvedTheme,
rgb })) or equivalent.

79-92: 💤 Low value

Optional: avoid DOM mutation by using a canvas to normalize colors.

parseCssColorToRgb works for the current input set (hex + rgb()/rgba()), but it appends/removes a <div> and triggers a style recompute on every theme/color change. A canvas-based normalizer is allocation-free and side-effect-free, and yields the same rgb(r, g, b[, a]) form back from fillStyle.

Also note the regex only handles integer comma-separated channels — rgb(0 0 0) (CSS Color 4 space-separated) and color(display-p3 …) (returned by some Chromium versions for wide-gamut inputs) would silently drop the title-bar update. Probably fine given current inputs, just worth being aware of.

♻️ Canvas-based alternative (no DOM mutation)
-function parseCssColorToRgb(value: string): [number, number, number] | null {
-  if (typeof document === "undefined") return null;
-  const probe = document.createElement("div");
-  probe.style.color = value;
-  probe.style.display = "none";
-  document.body.appendChild(probe);
-  const computed = getComputedStyle(probe).color;
-  document.body.removeChild(probe);
-  const match = computed.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
-  if (!match) return null;
-  return [Number(match[1]), Number(match[2]), Number(match[3])];
-}
+function parseCssColorToRgb(value: string): [number, number, number] | null {
+  if (typeof document === "undefined") return null;
+  const ctx = document.createElement("canvas").getContext("2d");
+  if (!ctx) return null;
+  ctx.fillStyle = "#000"; // reset to a known value first
+  ctx.fillStyle = value;  // canvas rejects invalid colors, leaving the previous value
+  const normalized = ctx.fillStyle as string;
+  const match = normalized.match(/rgba?\(\s*(\d+)[\s,]+(\d+)[\s,]+(\d+)/);
+  if (!match) return null;
+  return [Number(match[1]), Number(match[2]), Number(match[3])];
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/context/ThemeContext.tsx` around lines 79 - 92, parseCssColorToRgb
currently mutates the DOM by creating/removing a probe DIV and uses a limited
regex; replace its body with a canvas-based parser: if document/canvas is
unavailable return null, create an off-screen HTMLCanvasElement (or reuse a
cached one) and 2D context, set ctx.fillStyle = value, read back ctx.fillStyle
(which normalizes to an rgb(a) string), then parse that string with a more
robust regex that accepts comma- or space-separated channels and decimal values
(capture r,g,b as numbers, ignore alpha if present) and return [r,g,b] or null
on failure; reference the function name parseCssColorToRgb and ensure no DOM
mutations occur and that SSR environments safely return null.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/context/ThemeContext.tsx`:
- Around line 482-489: The catch on the invoke("set_title_bar_theme", ...) call
is swallowing errors; update the catch to log the error (e.g., console.error)
including context such as the function call and values like resolvedTheme and
rgb so failures in set_title_bar_theme are visible during debugging; locate the
block in ThemeContext.tsx where invoke("set_title_bar_theme", { isDark:
resolvedTheme === "dark", r: rgb[0], g: rgb[1], b: rgb[2] }) is called and
replace the empty .catch(() => {}) with a .catch(err =>
console.error("set_title_bar_theme failed", { err, resolvedTheme, rgb })) or
equivalent.
- Around line 79-92: parseCssColorToRgb currently mutates the DOM by
creating/removing a probe DIV and uses a limited regex; replace its body with a
canvas-based parser: if document/canvas is unavailable return null, create an
off-screen HTMLCanvasElement (or reuse a cached one) and 2D context, set
ctx.fillStyle = value, read back ctx.fillStyle (which normalizes to an rgb(a)
string), then parse that string with a more robust regex that accepts comma- or
space-separated channels and decimal values (capture r,g,b as numbers, ignore
alpha if present) and return [r,g,b] or null on failure; reference the function
name parseCssColorToRgb and ensure no DOM mutations occur and that SSR
environments safely return null.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 93a77b3a-aa96-43c8-b55a-df5ac6a29895

📥 Commits

Reviewing files that changed from the base of the PR and between a57ec54 and 469aa7d.

📒 Files selected for processing (2)
  • src-tauri/src/lib.rs
  • src/context/ThemeContext.tsx

Copy link
Copy Markdown
Owner

@erictli erictli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the contribution @chengcheng84 - I made it work with custom theme colors.

@erictli erictli merged commit 7c7bcdd into erictli:main May 5, 2026
2 checks passed
@chengcheng84 chengcheng84 deleted the fix/titlebar branch May 5, 2026 01:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants